package account

import (
	"context"
	"crypto/md5"
	"fmt"
	"gitee.com/chunanyong/zorm"
	"gitee.com/xinshortlink/internal/constants"
	"gitee.com/xinshortlink/internal/dal/cache"
	"gitee.com/xinshortlink/internal/dal/db/dao"
	"gitee.com/xinshortlink/internal/dal/db/model"
	"gitee.com/xinshortlink/internal/dto"
	"gitee.com/xinshortlink/internal/dto/resp/bizerr"
	"gitee.com/xinshortlink/internal/pkg/token"
	"github.com/go-redsync/redsync/v4"
	"github.com/redis/go-redis/v9"
	"time"

	"go.uber.org/zap"
)

const registerAccountKey = "register_account:"

type Service struct {
	logger     *zap.Logger
	accountDao *dao.AccountDao
	groupDao   *dao.GroupDao
	token      *token.Token
	loginUtil  *token.LoginAccountUtil
	rdb        *redis.Client
	rs         *redsync.Redsync
}

func NewService(logger *zap.Logger, accountDao *dao.AccountDao, groupDao *dao.GroupDao, token *token.Token,
	loginUtil *token.LoginAccountUtil, rdb *redis.Client, rs *redsync.Redsync) *Service {
	return &Service{
		logger:     logger,
		accountDao: accountDao,
		groupDao:   groupDao,
		token:      token,
		loginUtil:  loginUtil,
		rdb:        rdb,
		rs:         rs,
	}
}

// CreateAccount 创建账户
func (s *Service) CreateAccount(ctx context.Context, dto *dto.AccountDTO) error {
	// 恶意请求网关黑名单，ip 限流等

	// 布隆过滤器判断用户是否存在
	var exist bool
	exist, err := s.rdb.BFExists(ctx, constants.RegisterAccountBFKey, dto.AccountName).Result()
	if err != nil {
		// redis 异常
		s.logger.Error("BFExists fail", zap.Error(err))

		// 退化为数据库判断
		tmp, err := s.accountDao.FindByAccountName(ctx, dto.AccountName)
		if err != nil {
			return fmt.Errorf("SERVICE: CreateAccount fail %w", err)
		}

		exist = tmp != nil
	}

	if exist {
		return bizerr.ErrAccountNameExists
	}

	lockKey := registerAccountKey + dto.AccountName
	mutex := s.rs.NewMutex(lockKey, redsync.WithExpiry(5*time.Second), redsync.WithDriftFactor(0.005))
	if err := mutex.Lock(); err != nil {
		return fmt.Errorf("SERVICE: lock fail [lockKey=%s] %w", lockKey, bizerr.ErrAccountNameExists)
	}

	done := make(chan struct{})
	go cache.AutoExtend(mutex, done, s.logger) // 开启自动续期

	defer func() {
		close(done) // 停止续期

		for i := 0; i < 3; i++ {
			if ok, err := mutex.Unlock(); ok && err == nil {
				return
			}
			time.Sleep(100 * time.Millisecond)
			s.logger.Error("解锁失败", zap.String("lock_key", lockKey), zap.Error(err))
		}

		s.logger.Error("重试多次仍然解锁失败", zap.String("lock_key", lockKey))
	}()

	dto.Password = fmt.Sprintf("%x", md5.Sum([]byte(dto.Password)))

	account := &model.AccountStruct{
		AccountName: dto.AccountName,
		NickName:    dto.Nickname,
		Password:    dto.Password,
		Phone:       dto.Phone,
		Email:       dto.Email,
		CreateTime:  time.Now(),
		DelState:    constants.NO,
	}

	_, err = zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) {
		tmp, err := s.accountDao.FindByAccountName(ctx, dto.AccountName)
		if err != nil {
			return nil, err
		}

		if tmp != nil {
			_, err = s.rdb.BFAdd(ctx, constants.RegisterAccountBFKey, dto.AccountName).Result()
			if err != nil {
				return nil, err
			}
		}

		err = s.accountDao.Create(ctx, account)

		if err != nil {
			return nil, err
		}

		g := &model.GroupStruct{
			Name:        "默认分组",
			AccountName: account.AccountName,
			CreateTime:  time.Now(),
			UpdateTime:  time.Now(),
			DelState:    constants.NO,
		}

		err = s.groupDao.Create(ctx, g)

		if err != nil {
			return nil, err
		}

		_, err = s.rdb.BFAdd(ctx, constants.RegisterAccountBFKey, dto.AccountName).Result()

		return nil, err
	})
	if err != nil {
		return fmt.Errorf("SERVICE: CreateAccount fail %w", err)
	}

	return err
}

func (s *Service) Login(ctx context.Context, accountName, password, captcha string, remember bool) (*token.AccessToken, error) {
	temp, err := s.accountDao.FindByAccountName(ctx, accountName)
	if err != nil {
		return nil, fmt.Errorf("SERVICE: Login fail %w", err)
	}

	if temp == nil {
		return nil, bizerr.ErrAccountPassword
	}

	if temp.Password != fmt.Sprintf("%x", md5.Sum([]byte(password))) {
		return nil, bizerr.ErrAccountPassword
	}

	accessToken, err := s.token.Generate(ctx, &token.LoginAccount{
		Id:          temp.Id,
		AccountName: accountName,
		Nickname:    temp.NickName,
		Phone:       temp.Phone,
		Email:       temp.Email,
	})

	if err != nil {
		return nil, fmt.Errorf("SERVICE: Generate Token fail %w", err)
	}

	return accessToken, nil
}

func (s *Service) Logout(ctx context.Context) error {
	accountDTO := s.loginUtil.CurrentAccount(ctx)
	if accountDTO == nil {
		return bizerr.ErrTokenInvalid
	}
	err := s.token.Clear(ctx, accountDTO.AccessToken)

	if err != nil {
		return fmt.Errorf("SERVICE: Logout fail %w", err)
	}

	return nil
}

func (s *Service) CurrentAccount(ctx context.Context) (*token.LoginAccount, error) {
	accountDTO := s.loginUtil.CurrentAccount(ctx)
	if accountDTO == nil {
		return nil, bizerr.ErrTokenInvalid
	}

	account, err := s.FindAccountById(ctx, accountDTO.Id)
	if err != nil {
		return nil, fmt.Errorf("SERVICE: CurrentAccount fail %w", err)
	}
	return account, nil
}

func (s *Service) UpdateAccount(ctx context.Context, req *dto.AccountDTO) error {
	accountDTO := s.loginUtil.CurrentAccount(ctx)
	if accountDTO == nil {
		return bizerr.ErrTokenInvalid
	}

	_, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) {

		err := s.accountDao.Update(ctx, &model.AccountStruct{
			Id:       accountDTO.Id,
			NickName: req.Nickname,
			Phone:    req.Phone,
			Email:    req.Email,
		})

		return nil, err
	})

	if err != nil {
		return fmt.Errorf("SERVICE: UpdateAccount fail %w", err)
	}

	return err
}

func (s *Service) HasAccount(ctx context.Context, accountName string) (bool, error) {
	temp, err := s.accountDao.FindByAccountName(ctx, accountName)
	if err != nil {
		return false, fmt.Errorf("SERVICE: HasAccount fail %w", err)
	}

	return temp != nil, nil
}

func (s *Service) FindAccountById(ctx context.Context, id int64) (*token.LoginAccount, error) {
	temp, err := s.accountDao.FindAccountById(ctx, id)
	if err != nil {
		return nil, fmt.Errorf("SERVICE: FindAccountById fail %w", err)
	}

	if temp == nil {
		return nil, bizerr.ErrAccountNotFound
	}

	return &token.LoginAccount{
		Id:          temp.Id,
		AccountName: temp.AccountName,
		Nickname:    temp.NickName,
		Phone:       temp.Phone,
		Email:       temp.Email,
	}, nil
}
